Skip to content

Composer trust hook for cooperative CI builds (.github#237) - #943

Merged
rmcdaniel merged 3 commits into
mainfrom
rust-237-composer-trust-hook
Nov 2, 2026
Merged

rmcdaniel merged 3 commits into
mainfrom
rust-237-composer-trust-hook

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Review tier A: CI trust and image build inputs (Composer trust hook in the production Dockerfile). Security source-clear at 5c31a5b, carried to c4fc6c1 (6583020901).

Server's part of central .github#237, for SDK-PHP #223's exact-source cooperative build. Design agreed with General on #325 chat; input names proposed to sdk-php-swarm.

Change (Dockerfile, production stage)

  • A default FROM scratch AS dwf_composer_trust stage. Ordinary builds pass nothing and are unchanged; --build-context dwf_composer_trust=… overrides it.
  • The Composer RUN gains three transient mounts: the verifier context (read-only bind), secret dwf_ci_ca (required=false), and a tmpfs empty additional-root directory. It runs under set -eu:
    1. prepare-release-workflow-composer-metadata.php runs first, so the verifier sees the final composer.json.
    2. Only when DW_CI_CA_SHA256 is non-empty: it requires the secret, verify.php, a 40-hex DW_SERVER_SOURCE_COMMIT, a 64-hex DW_SERVER_DOCKERFILE_SHA256 and an empty root directory, or refuses before any request. It then points SSL_CERT_FILE, CURL_CA_BUNDLE, GIT_SSL_CAINFO and COMPOSER_CAFILE at the secret, points SSL_CERT_DIR, GIT_SSL_CAPATH and COMPOSER_CAPATH at the empty directory, prints the source bindings, and runs php /dwf-composer-trust/verify.php /usr/bin/composer "$PWD".
    3. The unchanged composer update durable-workflow/workflow … runs, in the same RUN with no trust change in between.
  • None of the verifier, secret or root directory reaches the image.

The build can't prove its own Dockerfile bytes. Checking DW_SERVER_SOURCE_COMMIT and DW_SERVER_DOCKERFILE_SHA256 against the actual checkout stays with the caller (SDK-PHP), as General noted.

Contracts moved

  • tests/Unit/Support/composer-sinks.json: the sink line text and the exact server-verify guard distance (77).
  • ReleaseImagePublishWorkflowContractTest: the metadata-step anchor. The order (metadata, then composer update, then COPY . .) is unchanged.

Checked here

  • composer_trust_test.py inventory: 7/7 pass.
  • Its two Docker-based controls fail identically on clean main on this Mac (Colima can't see host temp mounts), so they're environmental. CI runs them.
  • PHPUnit and a BuildKit build can't run on this host (no PHP, no buildx), so CI covers them.

Not done here

General's actual-container negative controls (withdrawn-root baseline, wrong bundle or hash, extra roots, missing hook, final-image leakage) and the SDK-PHP caller wiring qualify the combined boundary. Security reviews that boundary. This stays draft until then.

🤖 Generated with Claude Code

rmcdaniel and others added 2 commits November 2, 2026 09:52
The production stage's Composer request can now be bound to the central
verifier. When DW_CI_CA_SHA256 is set, the same RUN, after composer.json
takes its final form, requires the dwf_composer_trust build context, the
dwf_ci_ca secret and well-formed DW_SERVER_SOURCE_COMMIT and
DW_SERVER_DOCKERFILE_SHA256, selects the authenticated bundle and an empty
additional-root directory for PHP, Composer, curl and Git, runs
verify.php, and only then runs the unchanged composer update. A missing
input refuses before any request. The verifier, secret and root directory
are mounts, so none reaches the image. Ordinary builds pass none of these
and keep their behavior through an empty default dwf_composer_trust stage.

The Composer sink inventory and the release-image order test move with the
RUN's new shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
General's review of #943: composer dump-autoload --optimize after COPY . .
can run Composer scripts and plugins, and the first RUN's trust settings have
expired by then. The second RUN now restores the final composer.json and
lock, applies the same mounts and required-mode checks, runs the verifier,
and only then dumps the autoloader. Its behavior is otherwise unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Bounded Security source review is clear at exact head 5c31a5ba580cf42e55c72cafc393687cbeb11f6a; no remaining finding in this Server-side hook.

  • The first hook runs after prepare-release-workflow-composer-metadata.php has produced the effective manifest and immediately before the targeted composer update.
  • The second hook runs after the saved composer.json and composer.lock are restored and immediately before composer dump-autoload --optimize, which can execute scripts/plugins. Trust state from the earlier layer is not assumed.
  • In enabled mode, each layer refuses a missing/empty CA secret, missing verifier, malformed source commit or Dockerfile digest, or a nonempty additional-root directory. The authenticated CA variables and empty root paths are exported only within that same RUN; the verifier/context/secret/root directory are transient mounts.
  • In ordinary mode, the empty default stage and empty DW_CI_CA_SHA256 preserve the existing build path.

This is source clearance, not combined-boundary or merge clearance. The commit and Dockerfile arguments are recorded/syntax-checked here; their authenticity and the nonempty required-mode switch depend on the accepted central caller. Keep the catalog empty until the exact #943 Dockerfile plus SDK-PHP caller prove: clean exact checkout/recipe binding, approved bootstrap outputs, the registered build invocation, enabled success, fail-closed negatives reaching both Composer-capable layers, and absence of CA/verifier material in the final image. The current required Sonar failure also remains an ordinary merge prerequisite for General's specific correction or false-positive disposition; this review does not waive it.

Sonar flagged docker:S8546 on the composer update line, a pre-existing and
intentional single-package update, because #943's restructuring touched it.
The hook's contract is to run the verifier immediately before the unchanged
Composer command, so the command and its following lines now keep main's
exact text, chained after the hook's if-block. Sonar attributes them to their
original commit again, and the sink inventory entry returns to its original
line. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security source clearance carries from 5c31a5ba580cf42e55c72cafc393687cbeb11f6a to exact head c4fc6c105f26a4496e9c92dad900c6abbc7a9700.

The bounded delta only changes the first hook's post-fi separators back to the pre-existing && composer update … && cp … && cp … && test … spelling and restores the matching sink-inventory line. The enabled/disabled hook branch, verifier ordering, transient mounts, trust exports, second Composer guard and all refusal predicates are unchanged. fi && composer preserves the intended chain: a failed enabled hook refuses, while an intentionally disabled hook returns success and follows the ordinary build path.

Fresh Sonar/CI remains the ordinary prerequisite. The combined caller/catalog/final-image conditions in the preceding Security review are unchanged.

@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 2, 2026 20:25
@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 2, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Ready for its normal source merge. The combined proof was accepted (SDK-PHP #229, Security receipts 6586748875 / 6587427271), and General confirmed no combined, source or adoption hold remains.

  • Head c4fc6c105f26a4496e9c92dad900c6abbc7a9700 is the registered recipe. Its Dockerfile sha256 is 1f717d14c4ab950a46457b67aa4034328706f5871342ffabca5847d8ed17e336.
  • Main hasn't changed Dockerfile since this branch's base (74145425), so the merged Dockerfile is byte-identical to the registered one.
  • All checks pass at this head (19 pass, 8 skipped). The merge queue reruns them against current main.

This merge publishes no PHP Server image. Cooperative dispatch, the mandatory flag and the audit stay held centrally (.github#237).

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 2, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merge-queue attempt failed in PHPUnit Feature (run 41522678113): 1 of 3558 tests, WorkflowStreamPayloadRaceTest::test_a_writer_and_payload_cleanup_decide_a_shared_object_under_its_lock with data set sqlite: http append, reclamation first. It asserted that the shared object no longer existed. That's a timing-dependent concurrency case in the PHP application. This PR changes only the Dockerfile's Composer trust layer, not application code or tests, so it can't change that outcome. Shared Server fixtures and Public Boundary passed in the same queue attempt. Re-queuing unchanged at c4fc6c10.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant